package apple.core;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.log4j.Logger;

import apple.debug.EventRequest;
import apple.debug.ExceptionRequest;
import apple.debug.InstructionRequest;
import apple.debug.LocatableEventRequest;
import apple.debug.OpcodeRequest;
import apple.debug.StackFrame;
import apple.debug.ExceptionRequest.ExceptionType;

public class MOS6502Debug extends MOS6502 {

   static Logger logger = Logger.getLogger(MOS6502Debug.class);

   private Debugger debugger;

   private List<LocatableEventRequest> accessWatchpointRequests;

   private Map<Short, LocatableEventRequest> breakpointMap;

   private List<ExceptionRequest> exceptionRequests;

   private List<InstructionRequest> instructionRequests;

   private List<OpcodeRequest> opcodeRequests;

   private List<LocatableEventRequest> modificationWatchpointRequests;

   private Stack<StackFrame> frames;

   private boolean suspend = false;

   private boolean step = false;

   private boolean stepOver = false;

   public MOS6502Debug(Memory memory) {
      super(memory);
      breakpointMap = new HashMap<Short, LocatableEventRequest>();
      frames = new Stack<StackFrame>();
   }

   public void setDebugger(Debugger debugger) {
      this.debugger = debugger;
   }

   /*
    * The following methods are modeled after those found in
    * com.sun.jdi.VirtualMachine
    */

   public synchronized void suspend() {
      suspend = true;
   }

   public synchronized void resume() {
      suspend = false;
      step = false;
      notify();
   }

   public synchronized void stepNext() {
      suspend = false;
      step = true;
      notify();
   }

   /*
    * This needs to be changed to be implemented correctly.
    */
   public synchronized void stepOver() {
      suspend = false;
      /*
       * If the next instruction is not a JSR, stepOver has the same effect as
       * stepNext.
       */
      step = true;
      stepOver = true;
      notify();
   }

   /*
    * This needs to be changed to be implemented correctly.
    */
   public synchronized void stepReturn() {
      suspend = false;
      step = false;
      frames.peek().breakOnReturn = true;
      notify();
   }

   /*
    * The following methods are modeled after those found in
    * com.sun.jdi.request.EventRequestManager
    */
   public List<LocatableEventRequest> accessWatchpointRequests() {
      return Collections.unmodifiableList(accessWatchpointRequests);
   }

   public Collection<LocatableEventRequest> breakpointRequests() {
      return Collections.unmodifiableCollection(breakpointMap.values());
   }

   public LocatableEventRequest createAccessWatchpointRequest(short field) {
      LocatableEventRequest request = new LocatableEventRequest(field);
      accessWatchpointRequests.add(request);
      return request;
   }

   public LocatableEventRequest createBreakpointRequest(short location) {
      LocatableEventRequest request = new LocatableEventRequest(location);
      breakpointMap.put(location, request);
      return request;
   }

   public ExceptionRequest createExceptionRequest(ExceptionType exceptionType) {
      ExceptionRequest request = new ExceptionRequest(exceptionType);
      exceptionRequests.add(request);
      return request;
   }

   public InstructionRequest createInstructionRequest(String mnemonic) {
      InstructionRequest request = new InstructionRequest(mnemonic);
      instructionRequests.add(request);
      return request;
   }

   public OpcodeRequest createOpcodeRequest(byte opcode) {
      OpcodeRequest request = new OpcodeRequest(opcode);
      opcodeRequests.add(request);
      return request;
   }

   public LocatableEventRequest createModificationWatchpointRequest(short field) {
      LocatableEventRequest request = new LocatableEventRequest(field);
      modificationWatchpointRequests.add(request);
      return request;
   }

   public void setStep(boolean step) {
      this.step = step;
   }

   public void deleteAllBreakpoints() {
      breakpointMap.clear();
   }

   public void deleteEventRequest(EventRequest eventRequest) {

   }

   public void deleteEventRequests(List<? extends EventRequest> eventRequests) {

   }

   public List<ExceptionRequest> exceptionRequests() {
      return Collections.unmodifiableList(exceptionRequests);
   }

   public List<LocatableEventRequest> modificationWatchpointRequests() {
      return Collections.unmodifiableList(modificationWatchpointRequests);
   }

   /*
    * The following methods are modeled after those found in
    * com.sun.jdi.event.EventQueue
    */

   /*
    * The following method is modeled after that found in
    * com.sun.jdi.ThreadReference
    */

   public List<StackFrame> frames() {
      return Collections.unmodifiableList(frames);
   }

   private void checkStackFrames(byte newStackPointer) {
      boolean stackFrameRemoved = false;
      // need to check for empty stack first
      while (frames.peek().getStackPointer() < newStackPointer) {
         stackFrameRemoved = true;
         StackFrame frame = frames.pop();
         logger.debug(String.format(
               "Removed frame from stack: %04X %02X break=%b", frame
                     .getEntryAddress(), frame.getStackPointer(),
               frame.breakOnReturn));
         suspend = frame.breakOnReturn;
      }
      if (stackFrameRemoved) {
         dumpCallStack();
      }
   }

   protected byte pullByte() {
      checkStackFrames((byte) (stackPointer + 1));
      return super.pullByte();
   }

   public void reset() {
      super.reset();
      frames.clear();
      StackFrame frame = new StackFrame(programCounter, stackPointer);
      frames.push(frame);
   }

   protected void processInstruction() {
      if (step || breakpointMap.containsKey(programCounter)) {
         suspend = true;
      }
      if (suspend) {
         synchronized (this) {
            while (suspend) {
               try {
                  debugger.suspend();
                  wait();
               } catch (InterruptedException e) {
               }
            }
         }
      }
      /*
       * If stepOver was set by the debugger, it will be handled by the next
       * line if the next instruction is a JSR.
       */
      super.processInstruction();
      /*
       * Now clear stepOver so that it will not be mistakenly applied to any
       * later JSR.
       */
      stepOver = false;
   }

   protected void JSR(AddressingMode mode) {
      super.JSR(mode);
      StackFrame frame = new StackFrame(programCounter, stackPointer, stepOver);
      frames.push(frame);
      if (stepOver) {
         step = false;
      }
   }

   protected void RTS() {
      StackFrame frame = frames.peek();
      byte frameStackPointer = frame.getStackPointer();
      if (stackPointer == frameStackPointer) {
         // logger.debug("RTS: removing stack frame corresponding to JSR");
         frame = frames.pop();
         suspend = frame.breakOnReturn;
      } else if (stackPointer < frameStackPointer) {
         short exitAddress = (short) (programCounter - 1);
         logger.debug(String.format(
               "Debugger warning: %04X: RTS without corresponding JSR",
               exitAddress));
      } else {
         assert false : "Orphaned stack frames left on stack";
         System.exit(-1);
      }
      super.RTS();
   }

   protected void TXS() {
      if (stackPointer < indexRegisterX) {
         logger.debug(String.format(
               "TXS at %04X: stackPointer=%02X, indexRegisterX=%02X",
               (programCounter - 1) & 0xFFFF, stackPointer, indexRegisterX));
         checkStackFrames(indexRegisterX);
      }
      super.TXS();
   }

   private void dumpCallStack() {
      logger.debug("Dump of call stack - start");
      for (StackFrame frame : frames) {
         logger.debug(String.format("%04X : %02X", frame.getEntryAddress(),
               frame.getStackPointer()));
      }
      logger.debug("Dump of call stack - end");
   }
}
